package com.zzt.java_doc_search.searcher;

import lombok.extern.slf4j.Slf4j;

import java.io.*;
import java.util.ArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicLong;

/**
 * Created with IntelliJ IDEA.
 * Description
 * User: panda
 * Date: 2025-05-08
 * Time: 16:23
 */
@Slf4j
public class Parser {
    //先指定一个加载文档的路径
    private static final String INPUT_PATH = "D:/project/java_doc_search/java_doc_search_index/jdk-8u451-docs-all/docs/api";

    //创建一个 Index 类实例
    private Index index = new Index();

    private AtomicLong t1 = new AtomicLong(0);
    private AtomicLong t2 = new AtomicLong(0);

    //通过这个方法实现 “单线程制作索引”
    public void run(){
        long beg = System.currentTimeMillis();
        log.info("索引制作开始！");
        //整个 Parser 类的入口
        //1. 根据上面指定的路径，枚举出该路径的所有文件（html），这个过程需要把子目录中的文件都能获取到
        ArrayList<File> fileList = new ArrayList<>();
        enumFile(INPUT_PATH, fileList);
        long endEnumFile = System.currentTimeMillis();
        log.info("枚举文件完毕！消耗时间：" + (endEnumFile - beg) + "ms");
        //2. 针对上面罗列出的文件的路径，打开文件，读取文件内容，并进行解析，并构建索引。
        for (File file : fileList){
            //通过这个方法来解析单个的 HTML文件
            System.out.println("开始解析：" + file.getAbsolutePath());
            parseHTML(file);
        }
        long endFor = System.currentTimeMillis();
        log.info("遍历文件完毕！消耗时间：" + (endFor- endEnumFile) + "ms");
        //3. 把在内存中构造好的索引数据结构，保存到指定的文件当中。
        index.save();
        long end = System.currentTimeMillis();
        log.info("索引制作完毕！消耗时间：" + (end - beg) + "ms");
    }

    //通过这个方法实现 “多线程索引”
    public void runByThread() throws InterruptedException {
        long beg = System.currentTimeMillis();
        log.info("索引制作开始！");

        //1. 枚举出所有文件
        ArrayList<File> files = new ArrayList<>();
        enumFile(INPUT_PATH, files);
        //2. 循环遍历文件，此处为了能够通过多线程制作索引，就直接引入线程池
        CountDownLatch latch = new CountDownLatch(files.size());
        ExecutorService executorService = Executors.newFixedThreadPool(8);
        for(File file : files){
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    log.info("解析：" + file.getAbsolutePath());
                    parseHTML(file);
                    latch.countDown();
                }
            });
        }

        //await 方法会阻塞，直到所有的选手都调用 countDown 撞线之后，才能阻塞结束
        latch.await();

        //手动的把线程池的线程都干掉！
        executorService.shutdown();

        //3. 保存索引
        index.save();

        long end = System.currentTimeMillis();
        log.info("索引制作完毕！消耗时间：" + (end - beg) + "ms");
        log.info("t1:" + t1 + ",t2:" + t2);
    }

    private void parseHTML(File file) {
        //1. 解析出 HTML 的标题
        String title = parseTitle(file);
        //2. 解析出 HTML 对应的url
        String url = parseUrl(file);
        //3. 解析出 HTML 对应的正文（有了正文才有后续的描述）
        long beg = System.nanoTime();
//        String content = parseContent(file);
        String content = parseContentByRegex(file);
        long mid = System.nanoTime();
        //4. 把解析出来的这些信息，加入到索引当中
        index.addDoc(title, url, content);
        long end = System.nanoTime();
        //注意：由于 parseHTML 会被循环调用很多次，单次调用其实时间较短
        //加入频繁的打印可能会拖慢这个速度本身
        t1.addAndGet(mid - beg);
        t2.addAndGet(end - mid);
    }

    private String parseTitle(File file) {
        String name = file.getName();
        return name.substring(0, name.length() - ".html".length());
    }

    private String parseUrl(File file) {
        String part1 = "https://docs.oracle.com/javase/8/docs/api/";
        String part2 = file.getAbsolutePath().substring(INPUT_PATH.length());
        String result = part1 + part2;
        return result;
    }

    public String parseContent(File file) {
        //先按照一个字符一个字符的方式来读取，以 < 和 > 来控制拷贝数据的开关
        //手动把缓冲区设置成 1M 大小
        try(BufferedReader bufferedReader = new BufferedReader(new FileReader(file), 1024*1024)) {

            //加上是否需要拷贝的开关
            boolean isCopy = true;
            //还得准备一个保存结果的 StringBuilder
            StringBuilder content = new StringBuilder();
            while(true){
                //注意，此处的 read，返回值是一个 int ，而不是char
                //按理说，应该是一次读一个字符，就返回char就足够了
                //此处使用 int 作为返回值，主要是为了表示一些非法情况
                //如果读到了文件末尾，继续读，就会返回 -1
                int ret = bufferedReader.read();
                if(ret == -1){
                    //表示文件读完了
                    break;
                }
                //如果这个结果不是 -1 ，那么就是一个合法的字符了
                char c = (char)ret;
                if(isCopy){
                    //开关打开的状态，遇到普通的字符就应该拷贝到 StringBuilder 中
                    if(c == '<'){
                        isCopy = false;
                        continue;
                    }
                    //为了去掉换行，把换行替换成空格了
                    if(c == '\n' || c == '\r'){
                        c = ' ';
                    }
                    //其他字符，直接进行拷贝即可，把结果给拷贝到最终的 StringBuilder 中
                    content.append(c);
                }else{
                    //开关关闭的状态，就暂时不拷贝，直到遇到 >
                    if(c == '>'){
                        isCopy = true;
                    }
                }
            }
//            fileReader.close();
            return content.toString();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private String readFile(File file){
        try(BufferedReader bufferedReader = new BufferedReader(new FileReader(file))) {
            StringBuilder content = new StringBuilder();
            while(true){
                int ret = bufferedReader.read();
                if(ret == -1){
                    break;
                }
                char c = (char)ret;
                if(c == '\n' || c == '\r'){
                    c = ' ';
                }
                content.append(c);
            }
            return  content.toString();
        }catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    // 这个方法内部就基于正则表达式，实现去标签，以及去除 script
    public String parseContentByRegex(File file){
        // 1. 先把整个文件读到 String 里面
        String content = readFile(file);
        // 2. 替换掉 script 标签
        content = content.replaceAll("<script.*?>.*?</script>", " ");
        // 3. 替换掉普通的 html 标签
        content = content.replaceAll("<.*?>", " ");
        // 4. 使用正则表达式把多个空格，合并成一个空格
        content = content.replaceAll("\\s+", " ");
        return content;
    }


    /**
     * 递归遍历目录
     * @param inputPath 从哪个目录进行开始进行递归遍历
     * @param fileList 递归得到的结果
     */
    private void enumFile(String inputPath, ArrayList<File> fileList) {
        File rootPath = new File(inputPath);
        //listFiles 能够获取到 rootPath 当前目录下所包含的文件/目录
        //使用 listFiles 只能看到一级目录，看不到子目录里的内容
        //要想看到子目录中的内容，还需进行递归
        File[] files = rootPath.listFiles();
        for (File file : files){
            //根据当前 f 的类型，来决定是否需要递归
            //如果 f 是一个普通文件，就把 f 加入到 fileList 结果中
            //如果 f 是一个目录，就递归的调用 enumFile 这个方法，来进一步获取子目录的内容
            if(file.isDirectory()){
                enumFile(file.getAbsolutePath(), fileList);
            }else{
                if(file.getAbsolutePath().endsWith(".html")){
                    fileList.add(file);
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        //通过main方法来实现整个制作索引的过程
        Parser parser = new Parser();
        parser.runByThread();
    }
}
